[file_selector] Import Linux implementation from FDE (#6292)
diff --git a/.cirrus.yml b/.cirrus.yml
index 1ac69d6..059f5fe 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -212,9 +212,9 @@
- flutter config --enable-linux-desktop
- ./script/tool_runner.sh build-examples --linux
native_test_script:
- - ./script/tool_runner.sh native-test --linux --no-integration
+ - xvfb-run ./script/tool_runner.sh native-test --linux --no-integration
drive_script:
- - xvfb-run ./script/tool_runner.sh drive-examples --linux
+ - xvfb-run ./script/tool_runner.sh drive-examples --linux --exclude=script/configs/exclude_integration_linux.yaml
# Heavy-workload Linux tasks.
# These use machines with more CPUs and memory, so will reduce parallelization
diff --git a/packages/file_selector/file_selector_linux/.gitignore b/packages/file_selector/file_selector_linux/.gitignore
new file mode 100644
index 0000000..0393a47
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/.gitignore
@@ -0,0 +1,5 @@
+.dart_tool
+.packages
+.flutter-plugins
+.flutter-plugins-dependencies
+pubspec.lock
diff --git a/packages/file_selector/file_selector_linux/AUTHORS b/packages/file_selector/file_selector_linux/AUTHORS
new file mode 100644
index 0000000..557dff9
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/AUTHORS
@@ -0,0 +1,6 @@
+# Below is a list of people and organizations that have contributed
+# to the Flutter project. Names should be added to the list like so:
+#
+# Name/Organization <email address>
+
+Google Inc.
diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md
new file mode 100644
index 0000000..a63a52d
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/CHANGELOG.md
@@ -0,0 +1,19 @@
+## 0.9.0
+
+* Moves source to flutter/plugins.
+
+## 0.0.3
+
+* Adds Dart implementation for in-package method channel.
+
+## 0.0.2+1
+
+* Updates README
+
+## 0.0.2
+
+* Updates SDK constraint to signal compatibility with null safety.
+
+## 0.0.1
+
+* Initial Linux implementation of `file_selector`.
diff --git a/packages/file_selector/file_selector_linux/LICENSE b/packages/file_selector/file_selector_linux/LICENSE
new file mode 100644
index 0000000..c6823b8
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2013 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/file_selector/file_selector_linux/README.md b/packages/file_selector/file_selector_linux/README.md
new file mode 100644
index 0000000..55a0529
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/README.md
@@ -0,0 +1,11 @@
+# file\_selector\_linux
+
+The Linux implementation of [`file_selector`][1].
+
+## Usage
+
+This package is [endorsed][2], which means you can simply use `file_selector`
+normally. This package will be automatically included in your app when you do.
+
+[1]: https://pub.dev/packages/file_selector
+[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
diff --git a/packages/file_selector/file_selector_linux/example/.gitignore b/packages/file_selector/file_selector_linux/example/.gitignore
new file mode 100644
index 0000000..7abd075
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/.gitignore
@@ -0,0 +1,48 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Currently only web supported
+android/
+ios/
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/file_selector/file_selector_linux/example/.metadata b/packages/file_selector/file_selector_linux/example/.metadata
new file mode 100644
index 0000000..897381f
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 7736f3bc90270dcb0480db2ccffbf1d13c28db85
+ channel: dev
+
+project_type: app
diff --git a/packages/file_selector/file_selector_linux/example/README.md b/packages/file_selector/file_selector_linux/example/README.md
new file mode 100644
index 0000000..2f9f8c0
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/README.md
@@ -0,0 +1,4 @@
+# `file_selector_linux` example
+
+Demonstrates Linux implementation of the
+[`file_selector` plugin](https://pub.dev/packages/file_selector).
diff --git a/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart
new file mode 100644
index 0000000..0699dd1
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart
@@ -0,0 +1,83 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select a directory using `getDirectoryPath`,
+/// then displays the selected directory in a dialog.
+class GetDirectoryPage extends StatelessWidget {
+ /// Default Constructor
+ const GetDirectoryPage({Key? key}) : super(key: key);
+
+ Future<void> _getDirectoryPath(BuildContext context) async {
+ const String confirmButtonText = 'Choose';
+ final String? directoryPath =
+ await FileSelectorPlatform.instance.getDirectoryPath(
+ confirmButtonText: confirmButtonText,
+ );
+ if (directoryPath == null) {
+ // Operation was canceled by the user.
+ return;
+ }
+ await showDialog<void>(
+ context: context,
+ builder: (BuildContext context) => TextDisplay(directoryPath),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Open a text file'),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: Colors.blue,
+ // ignore: deprecated_member_use
+ onPrimary: Colors.white,
+ ),
+ child: const Text('Press to ask user to choose a directory'),
+ onPressed: () => _getDirectoryPath(context),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// Widget that displays a text file in a dialog.
+class TextDisplay extends StatelessWidget {
+ /// Creates a `TextDisplay`.
+ const TextDisplay(this.directoryPath, {Key? key}) : super(key: key);
+
+ /// The path selected in the dialog.
+ final String directoryPath;
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: const Text('Selected Directory'),
+ content: Scrollbar(
+ child: SingleChildScrollView(
+ child: Text(directoryPath),
+ ),
+ ),
+ actions: <Widget>[
+ TextButton(
+ child: const Text('Close'),
+ onPressed: () => Navigator.pop(context),
+ ),
+ ],
+ );
+ }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/home_page.dart b/packages/file_selector/file_selector_linux/example/lib/home_page.dart
new file mode 100644
index 0000000..a4b2ae1
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/home_page.dart
@@ -0,0 +1,63 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+/// Home Page of the application.
+class HomePage extends StatelessWidget {
+ /// Default Constructor
+ const HomePage({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final ButtonStyle style = ElevatedButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: Colors.blue,
+ // ignore: deprecated_member_use
+ onPrimary: Colors.white,
+ );
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('File Selector Demo Home Page'),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ ElevatedButton(
+ style: style,
+ child: const Text('Open a text file'),
+ onPressed: () => Navigator.pushNamed(context, '/open/text'),
+ ),
+ const SizedBox(height: 10),
+ ElevatedButton(
+ style: style,
+ child: const Text('Open an image'),
+ onPressed: () => Navigator.pushNamed(context, '/open/image'),
+ ),
+ const SizedBox(height: 10),
+ ElevatedButton(
+ style: style,
+ child: const Text('Open multiple images'),
+ onPressed: () => Navigator.pushNamed(context, '/open/images'),
+ ),
+ const SizedBox(height: 10),
+ ElevatedButton(
+ style: style,
+ child: const Text('Save a file'),
+ onPressed: () => Navigator.pushNamed(context, '/save/text'),
+ ),
+ const SizedBox(height: 10),
+ ElevatedButton(
+ style: style,
+ child: const Text('Open a get directory dialog'),
+ onPressed: () => Navigator.pushNamed(context, '/directory'),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/main.dart b/packages/file_selector/file_selector_linux/example/lib/main.dart
new file mode 100644
index 0000000..3e44710
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/main.dart
@@ -0,0 +1,42 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+import 'get_directory_page.dart';
+import 'home_page.dart';
+import 'open_image_page.dart';
+import 'open_multiple_images_page.dart';
+import 'open_text_page.dart';
+import 'save_text_page.dart';
+
+void main() {
+ runApp(const MyApp());
+}
+
+/// MyApp is the Main Application.
+class MyApp extends StatelessWidget {
+ /// Default Constructor
+ const MyApp({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'File Selector Demo',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ visualDensity: VisualDensity.adaptivePlatformDensity,
+ ),
+ home: const HomePage(),
+ routes: <String, WidgetBuilder>{
+ '/open/image': (BuildContext context) => const OpenImagePage(),
+ '/open/images': (BuildContext context) =>
+ const OpenMultipleImagesPage(),
+ '/open/text': (BuildContext context) => const OpenTextPage(),
+ '/save/text': (BuildContext context) => SaveTextPage(),
+ '/directory': (BuildContext context) => const GetDirectoryPage(),
+ },
+ );
+ }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart
new file mode 100644
index 0000000..9e1d207
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart
@@ -0,0 +1,94 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select an image file using
+/// `openFiles`, then displays the selected images in a gallery dialog.
+class OpenImagePage extends StatelessWidget {
+ /// Default Constructor
+ const OpenImagePage({Key? key}) : super(key: key);
+
+ Future<void> _openImageFile(BuildContext context) async {
+ final XTypeGroup typeGroup = XTypeGroup(
+ label: 'images',
+ extensions: <String>['jpg', 'png'],
+ );
+ final XFile? file = await FileSelectorPlatform.instance
+ .openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
+ if (file == null) {
+ // Operation was canceled by the user.
+ return;
+ }
+ final String fileName = file.name;
+ final String filePath = file.path;
+
+ await showDialog<void>(
+ context: context,
+ builder: (BuildContext context) => ImageDisplay(fileName, filePath),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Open an image'),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: Colors.blue,
+ // ignore: deprecated_member_use
+ onPrimary: Colors.white,
+ ),
+ child: const Text('Press to open an image file(png, jpg)'),
+ onPressed: () => _openImageFile(context),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// Widget that displays an image in a dialog.
+class ImageDisplay extends StatelessWidget {
+ /// Default Constructor.
+ const ImageDisplay(this.fileName, this.filePath, {Key? key})
+ : super(key: key);
+
+ /// The name of the selected file.
+ final String fileName;
+
+ /// The path to the selected file.
+ final String filePath;
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: Text(fileName),
+ // On web the filePath is a blob url
+ // while on other platforms it is a system path.
+ content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)),
+ actions: <Widget>[
+ TextButton(
+ child: const Text('Close'),
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ );
+ }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart
new file mode 100644
index 0000000..21da8c2
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart
@@ -0,0 +1,105 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select multiple image files using
+/// `openFiles`, then displays the selected images in a gallery dialog.
+class OpenMultipleImagesPage extends StatelessWidget {
+ /// Default Constructor
+ const OpenMultipleImagesPage({Key? key}) : super(key: key);
+
+ Future<void> _openImageFile(BuildContext context) async {
+ final XTypeGroup jpgsTypeGroup = XTypeGroup(
+ label: 'JPEGs',
+ extensions: <String>['jpg', 'jpeg'],
+ );
+ final XTypeGroup pngTypeGroup = XTypeGroup(
+ label: 'PNGs',
+ extensions: <String>['png'],
+ );
+ final List<XFile> files = await FileSelectorPlatform.instance
+ .openFiles(acceptedTypeGroups: <XTypeGroup>[
+ jpgsTypeGroup,
+ pngTypeGroup,
+ ]);
+ if (files.isEmpty) {
+ // Operation was canceled by the user.
+ return;
+ }
+ await showDialog<void>(
+ context: context,
+ builder: (BuildContext context) => MultipleImagesDisplay(files),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Open multiple images'),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: Colors.blue,
+ // ignore: deprecated_member_use
+ onPrimary: Colors.white,
+ ),
+ child: const Text('Press to open multiple images (png, jpg)'),
+ onPressed: () => _openImageFile(context),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// Widget that displays a text file in a dialog.
+class MultipleImagesDisplay extends StatelessWidget {
+ /// Default Constructor.
+ const MultipleImagesDisplay(this.files, {Key? key}) : super(key: key);
+
+ /// The files containing the images.
+ final List<XFile> files;
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: const Text('Gallery'),
+ // On web the filePath is a blob url
+ // while on other platforms it is a system path.
+ content: Center(
+ child: Row(
+ children: <Widget>[
+ ...files.map(
+ (XFile file) => Flexible(
+ child: kIsWeb
+ ? Image.network(file.path)
+ : Image.file(File(file.path))),
+ )
+ ],
+ ),
+ ),
+ actions: <Widget>[
+ TextButton(
+ child: const Text('Close'),
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ );
+ }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart
new file mode 100644
index 0000000..05c6d16
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart
@@ -0,0 +1,91 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select a text file using `openFile`, then
+/// displays its contents in a dialog.
+class OpenTextPage extends StatelessWidget {
+ /// Default Constructor
+ const OpenTextPage({Key? key}) : super(key: key);
+
+ Future<void> _openTextFile(BuildContext context) async {
+ final XTypeGroup typeGroup = XTypeGroup(
+ label: 'text',
+ extensions: <String>['txt', 'json'],
+ );
+ final XFile? file = await FileSelectorPlatform.instance
+ .openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
+ if (file == null) {
+ // Operation was canceled by the user.
+ return;
+ }
+ final String fileName = file.name;
+ final String fileContent = await file.readAsString();
+
+ await showDialog<void>(
+ context: context,
+ builder: (BuildContext context) => TextDisplay(fileName, fileContent),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Open a text file'),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: Colors.blue,
+ // ignore: deprecated_member_use
+ onPrimary: Colors.white,
+ ),
+ child: const Text('Press to open a text file (json, txt)'),
+ onPressed: () => _openTextFile(context),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// Widget that displays a text file in a dialog.
+class TextDisplay extends StatelessWidget {
+ /// Default Constructor.
+ const TextDisplay(this.fileName, this.fileContent, {Key? key})
+ : super(key: key);
+
+ /// The name of the selected file.
+ final String fileName;
+
+ /// The contents of the text file.
+ final String fileContent;
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: Text(fileName),
+ content: Scrollbar(
+ child: SingleChildScrollView(
+ child: Text(fileContent),
+ ),
+ ),
+ actions: <Widget>[
+ TextButton(
+ child: const Text('Close'),
+ onPressed: () => Navigator.pop(context),
+ ),
+ ],
+ );
+ }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart
new file mode 100644
index 0000000..9803f28
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart
@@ -0,0 +1,84 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:typed_data';
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select a save location using `getSavePath`,
+/// then writes text to a file at that location.
+class SaveTextPage extends StatelessWidget {
+ /// Default Constructor
+ SaveTextPage({Key? key}) : super(key: key);
+
+ final TextEditingController _nameController = TextEditingController();
+ final TextEditingController _contentController = TextEditingController();
+
+ Future<void> _saveFile() async {
+ final String fileName = _nameController.text;
+ final String? path = await FileSelectorPlatform.instance.getSavePath(
+ // Operation was canceled by the user.
+ suggestedName: fileName,
+ );
+ if (path == null) {
+ return;
+ }
+ final String text = _contentController.text;
+ final Uint8List fileData = Uint8List.fromList(text.codeUnits);
+ const String fileMimeType = 'text/plain';
+ final XFile textFile =
+ XFile.fromData(fileData, mimeType: fileMimeType, name: fileName);
+ await textFile.saveTo(path);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Save text into a file'),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ Container(
+ width: 300,
+ child: TextField(
+ minLines: 1,
+ maxLines: 12,
+ controller: _nameController,
+ decoration: const InputDecoration(
+ hintText: '(Optional) Suggest File Name',
+ ),
+ ),
+ ),
+ Container(
+ width: 300,
+ child: TextField(
+ minLines: 1,
+ maxLines: 12,
+ controller: _contentController,
+ decoration: const InputDecoration(
+ hintText: 'Enter File Contents',
+ ),
+ ),
+ ),
+ const SizedBox(height: 10),
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: Colors.blue,
+ // ignore: deprecated_member_use
+ onPrimary: Colors.white,
+ ),
+ onPressed: _saveFile,
+ child: const Text('Press to save a text file'),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/file_selector/file_selector_linux/example/linux/.gitignore b/packages/file_selector/file_selector_linux/example/linux/.gitignore
new file mode 100644
index 0000000..d3896c9
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/packages/file_selector/file_selector_linux/example/linux/CMakeLists.txt b/packages/file_selector/file_selector_linux/example/linux/CMakeLists.txt
new file mode 100644
index 0000000..9d7224c
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/CMakeLists.txt
@@ -0,0 +1,111 @@
+cmake_minimum_required(VERSION 3.10)
+project(runner LANGUAGES CXX)
+
+set(BINARY_NAME "example")
+set(APPLICATION_ID "com.example.example")
+
+cmake_policy(SET CMP0063 NEW)
+
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Configure build options.
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+endif()
+
+# Compilation settings that should be applied to most targets.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_14)
+ target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+ target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
+ target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
+endfunction()
+
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+
+# Flutter library and tool build rules.
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Application build
+add_executable(${BINARY_NAME}
+ "main.cc"
+ "my_application.cc"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+apply_standard_settings(${BINARY_NAME})
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+add_dependencies(${BINARY_NAME} flutter_assemble)
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+ PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+# Enable the test target.
+set(include_file_selector_linux_tests TRUE)
+# Provide an alias for the test target using the name expected by repo tooling.
+add_custom_target(unit_tests DEPENDS file_selector_linux_test)
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+ file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+ " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+ install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+ install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
diff --git a/packages/file_selector/file_selector_linux/example/linux/flutter/CMakeLists.txt b/packages/file_selector/file_selector_linux/example/linux/flutter/CMakeLists.txt
new file mode 100644
index 0000000..33fd580
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/flutter/CMakeLists.txt
@@ -0,0 +1,87 @@
+cmake_minimum_required(VERSION 3.10)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+ set(NEW_LIST "")
+ foreach(element ${${LIST_NAME}})
+ list(APPEND NEW_LIST "${PREFIX}${element}")
+ endforeach(element)
+ set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "fl_basic_message_channel.h"
+ "fl_binary_codec.h"
+ "fl_binary_messenger.h"
+ "fl_dart_project.h"
+ "fl_engine.h"
+ "fl_json_message_codec.h"
+ "fl_json_method_codec.h"
+ "fl_message_codec.h"
+ "fl_method_call.h"
+ "fl_method_channel.h"
+ "fl_method_codec.h"
+ "fl_method_response.h"
+ "fl_plugin_registrar.h"
+ "fl_plugin_registry.h"
+ "fl_standard_message_codec.h"
+ "fl_standard_method_codec.h"
+ "fl_string_codec.h"
+ "fl_value.h"
+ "fl_view.h"
+ "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+ PkgConfig::GTK
+ PkgConfig::GLIB
+ PkgConfig::GIO
+)
+add_dependencies(flutter flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+ ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+)
diff --git a/packages/file_selector/file_selector_linux/example/linux/flutter/generated_plugins.cmake b/packages/file_selector/file_selector_linux/example/linux/flutter/generated_plugins.cmake
new file mode 100644
index 0000000..2db3c22
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/flutter/generated_plugins.cmake
@@ -0,0 +1,24 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+ file_selector_linux
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/packages/file_selector/file_selector_linux/example/linux/main.cc b/packages/file_selector/file_selector_linux/example/linux/main.cc
new file mode 100644
index 0000000..1507d02
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/main.cc
@@ -0,0 +1,10 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+ g_autoptr(MyApplication) app = my_application_new();
+ return g_application_run(G_APPLICATION(app), argc, argv);
+}
diff --git a/packages/file_selector/file_selector_linux/example/linux/my_application.cc b/packages/file_selector/file_selector_linux/example/linux/my_application.cc
new file mode 100644
index 0000000..e970be0
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/my_application.cc
@@ -0,0 +1,110 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "my_application.h"
+
+#include <flutter_linux/flutter_linux.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+ GtkApplication parent_instance;
+ char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+ MyApplication* self = MY_APPLICATION(application);
+ GtkWindow* window =
+ GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+ // Use a header bar when running in GNOME as this is the common style used
+ // by applications and is the setup most users will be using (e.g. Ubuntu
+ // desktop).
+ // If running on X and not using GNOME then just use a traditional title bar
+ // in case the window manager does more exotic layout, e.g. tiling.
+ // If running on Wayland assume the header bar will work (may need changing
+ // if future cases occur).
+ gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+ GdkScreen* screen = gtk_window_get_screen(window);
+ if (GDK_IS_X11_SCREEN(screen)) {
+ const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+ if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+ use_header_bar = FALSE;
+ }
+ }
+#endif
+ if (use_header_bar) {
+ GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+ gtk_widget_show(GTK_WIDGET(header_bar));
+ gtk_header_bar_set_title(header_bar, "example");
+ gtk_header_bar_set_show_close_button(header_bar, TRUE);
+ gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+ } else {
+ gtk_window_set_title(window, "example");
+ }
+
+ gtk_window_set_default_size(window, 1280, 720);
+ gtk_widget_show(GTK_WIDGET(window));
+
+ g_autoptr(FlDartProject) project = fl_dart_project_new();
+ fl_dart_project_set_dart_entrypoint_arguments(
+ project, self->dart_entrypoint_arguments);
+
+ FlView* view = fl_view_new(project);
+ gtk_widget_show(GTK_WIDGET(view));
+ gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+ fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+ gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application,
+ gchar*** arguments,
+ int* exit_status) {
+ MyApplication* self = MY_APPLICATION(application);
+ // Strip out the first argument as it is the binary name.
+ self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+ g_autoptr(GError) error = nullptr;
+ if (!g_application_register(application, nullptr, &error)) {
+ g_warning("Failed to register: %s", error->message);
+ *exit_status = 1;
+ return TRUE;
+ }
+
+ g_application_activate(application);
+ *exit_status = 0;
+
+ return TRUE;
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+ MyApplication* self = MY_APPLICATION(object);
+ g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+ G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+ G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+ G_APPLICATION_CLASS(klass)->local_command_line =
+ my_application_local_command_line;
+ G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+ return MY_APPLICATION(g_object_new(
+ my_application_get_type(), "application-id", APPLICATION_ID, nullptr));
+}
diff --git a/packages/file_selector/file_selector_linux/example/linux/my_application.h b/packages/file_selector/file_selector_linux/example/linux/my_application.h
new file mode 100644
index 0000000..6e9f0c3
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/my_application.h
@@ -0,0 +1,22 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include <gtk/gtk.h>
+
+G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
+ GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif // FLUTTER_MY_APPLICATION_H_
diff --git a/packages/file_selector/file_selector_linux/example/pubspec.yaml b/packages/file_selector/file_selector_linux/example/pubspec.yaml
new file mode 100644
index 0000000..857c950
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/pubspec.yaml
@@ -0,0 +1,21 @@
+name: file_selector_linux_example
+description: Local testbed for Linux file_selector implementation.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+ file_selector_linux:
+ path: ../
+ file_selector_platform_interface: ^2.0.0
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
diff --git a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart
new file mode 100644
index 0000000..430b41c
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart
@@ -0,0 +1,144 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/foundation.dart' show visibleForTesting;
+import 'package:flutter/services.dart';
+
+const MethodChannel _channel =
+ MethodChannel('plugins.flutter.dev/file_selector_linux');
+
+const String _typeGroupLabelKey = 'label';
+const String _typeGroupExtensionsKey = 'extensions';
+const String _typeGroupMimeTypesKey = 'mimeTypes';
+
+const String _openFileMethod = 'openFile';
+const String _getSavePathMethod = 'getSavePath';
+const String _getDirectoryPathMethod = 'getDirectoryPath';
+
+const String _acceptedTypeGroupsKey = 'acceptedTypeGroups';
+const String _confirmButtonTextKey = 'confirmButtonText';
+const String _initialDirectoryKey = 'initialDirectory';
+const String _multipleKey = 'multiple';
+const String _suggestedNameKey = 'suggestedName';
+
+/// An implementation of [FileSelectorPlatform] for Linux.
+class FileSelectorLinux extends FileSelectorPlatform {
+ /// The MethodChannel that is being used by this implementation of the plugin.
+ @visibleForTesting
+ MethodChannel get channel => _channel;
+
+ /// Registers the Linux implementation.
+ static void registerWith() {
+ FileSelectorPlatform.instance = FileSelectorLinux();
+ }
+
+ @override
+ Future<XFile?> openFile({
+ List<XTypeGroup>? acceptedTypeGroups,
+ String? initialDirectory,
+ String? confirmButtonText,
+ }) async {
+ final List<Map<String, Object>> serializedTypeGroups =
+ _serializeTypeGroups(acceptedTypeGroups);
+ final List<String>? path = await _channel.invokeListMethod<String>(
+ _openFileMethod,
+ <String, dynamic>{
+ if (serializedTypeGroups.isNotEmpty)
+ _acceptedTypeGroupsKey: serializedTypeGroups,
+ 'initialDirectory': initialDirectory,
+ _confirmButtonTextKey: confirmButtonText,
+ _multipleKey: false,
+ },
+ );
+ return path == null ? null : XFile(path.first);
+ }
+
+ @override
+ Future<List<XFile>> openFiles({
+ List<XTypeGroup>? acceptedTypeGroups,
+ String? initialDirectory,
+ String? confirmButtonText,
+ }) async {
+ final List<Map<String, Object>> serializedTypeGroups =
+ _serializeTypeGroups(acceptedTypeGroups);
+ final List<String>? pathList = await _channel.invokeListMethod<String>(
+ _openFileMethod,
+ <String, dynamic>{
+ if (serializedTypeGroups.isNotEmpty)
+ _acceptedTypeGroupsKey: serializedTypeGroups,
+ _initialDirectoryKey: initialDirectory,
+ _confirmButtonTextKey: confirmButtonText,
+ _multipleKey: true,
+ },
+ );
+ return pathList?.map((String path) => XFile(path)).toList() ?? <XFile>[];
+ }
+
+ @override
+ Future<String?> getSavePath({
+ List<XTypeGroup>? acceptedTypeGroups,
+ String? initialDirectory,
+ String? suggestedName,
+ String? confirmButtonText,
+ }) async {
+ final List<Map<String, Object>> serializedTypeGroups =
+ _serializeTypeGroups(acceptedTypeGroups);
+ return _channel.invokeMethod<String>(
+ _getSavePathMethod,
+ <String, dynamic>{
+ if (serializedTypeGroups.isNotEmpty)
+ _acceptedTypeGroupsKey: serializedTypeGroups,
+ _initialDirectoryKey: initialDirectory,
+ _suggestedNameKey: suggestedName,
+ _confirmButtonTextKey: confirmButtonText,
+ },
+ );
+ }
+
+ @override
+ Future<String?> getDirectoryPath({
+ String? initialDirectory,
+ String? confirmButtonText,
+ }) async {
+ return _channel.invokeMethod<String>(
+ _getDirectoryPathMethod,
+ <String, dynamic>{
+ _initialDirectoryKey: initialDirectory,
+ _confirmButtonTextKey: confirmButtonText,
+ },
+ );
+ }
+}
+
+List<Map<String, Object>> _serializeTypeGroups(List<XTypeGroup>? groups) {
+ return (groups ?? <XTypeGroup>[]).map(_serializeTypeGroup).toList();
+}
+
+Map<String, Object> _serializeTypeGroup(XTypeGroup group) {
+ final Map<String, Object> serialization = <String, Object>{
+ _typeGroupLabelKey: group.label ?? '',
+ };
+ if (group.allowsAny) {
+ serialization[_typeGroupExtensionsKey] = <String>['*'];
+ } else {
+ if ((group.extensions?.isEmpty ?? true) &&
+ (group.mimeTypes?.isEmpty ?? true)) {
+ throw ArgumentError('Provided type group $group does not allow '
+ 'all files, but does not set any of the Linux-supported filter '
+ 'categories. "extensions" or "mimeTypes" must be non-empty for Linux '
+ 'if anything is non-empty.');
+ }
+ if (group.extensions?.isNotEmpty ?? false) {
+ serialization[_typeGroupExtensionsKey] = group.extensions
+ ?.map((String extension) => '*.$extension')
+ .toList() ??
+ <String>[];
+ }
+ if (group.mimeTypes?.isNotEmpty ?? false) {
+ serialization[_typeGroupMimeTypesKey] = group.mimeTypes ?? <String>[];
+ }
+ }
+ return serialization;
+}
diff --git a/packages/file_selector/file_selector_linux/linux/CMakeLists.txt b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt
new file mode 100644
index 0000000..d0316d9
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt
@@ -0,0 +1,67 @@
+cmake_minimum_required(VERSION 3.10)
+set(PROJECT_NAME "file_selector_linux")
+project(${PROJECT_NAME} LANGUAGES CXX)
+
+set(PLUGIN_NAME "${PROJECT_NAME}_plugin")
+
+list(APPEND PLUGIN_SOURCES
+ "file_selector_plugin.cc"
+)
+
+add_library(${PLUGIN_NAME} SHARED
+ "file_selector_plugin.cc"
+)
+apply_standard_settings(${PLUGIN_NAME})
+set_target_properties(${PLUGIN_NAME} PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
+target_include_directories(${PLUGIN_NAME} INTERFACE
+ "${CMAKE_CURRENT_SOURCE_DIR}/include")
+target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
+target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
+
+
+# === Tests ===
+
+if(${include_${PROJECT_NAME}_tests})
+if(${CMAKE_VERSION} VERSION_LESS "3.11.0")
+message("Unit tests require CMake 3.11.0 or later")
+else()
+set(TEST_RUNNER "${PROJECT_NAME}_test")
+enable_testing()
+# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest
+# instance rather than downloading for each plugin. This approach makes sense
+# for a template, but not for a monorepo with many plugins.
+include(FetchContent)
+FetchContent_Declare(
+ googletest
+ URL https://github.com/google/googletest/archive/release-1.11.0.zip
+)
+# Prevent overriding the parent project's compiler/linker settings
+set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
+# Disable install commands for gtest so it doesn't end up in the bundle.
+set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE)
+
+FetchContent_MakeAvailable(googletest)
+
+# The plugin's exported API is not very useful for unit testing, so build the
+# sources directly into the test binary rather than using the shared library.
+add_executable(${TEST_RUNNER}
+ test/file_selector_plugin_test.cc
+ test/test_main.cc
+ ${PLUGIN_SOURCES}
+)
+apply_standard_settings(${TEST_RUNNER})
+target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
+target_link_libraries(${TEST_RUNNER} PRIVATE flutter)
+target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GTK)
+target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
+
+include(GoogleTest)
+# TODO(stuartmorgan): Switch back to gtest_discover_tests when moving to
+# flutter/plugins; it doesn't work in the FDE CI because it requires actually
+# running a GTK app, which it hasn't been set up for.
+gtest_add_tests(TARGET ${TEST_RUNNER})
+#gtest_discover_tests(${TEST_RUNNER})
+endif() # CMake version check
+endif() # include_${PROJECT_NAME}_tests
diff --git a/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc
new file mode 100644
index 0000000..8337719
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc
@@ -0,0 +1,246 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "include/file_selector_linux/file_selector_plugin.h"
+
+#include <flutter_linux/flutter_linux.h>
+#include <gtk/gtk.h>
+
+#include "file_selector_plugin_private.h"
+
+// From file_selector_linux.dart
+const char kChannelName[] = "plugins.flutter.dev/file_selector_linux";
+
+const char kOpenFileMethod[] = "openFile";
+const char kGetSavePathMethod[] = "getSavePath";
+const char kGetDirectoryPathMethod[] = "getDirectoryPath";
+
+const char kAcceptedTypeGroupsKey[] = "acceptedTypeGroups";
+const char kConfirmButtonTextKey[] = "confirmButtonText";
+const char kInitialDirectoryKey[] = "initialDirectory";
+const char kMultipleKey[] = "multiple";
+const char kSuggestedNameKey[] = "suggestedName";
+
+const char kTypeGroupLabelKey[] = "label";
+const char kTypeGroupExtensionsKey[] = "extensions";
+const char kTypeGroupMimeTypesKey[] = "mimeTypes";
+
+// Errors
+const char kBadArgumentsError[] = "Bad Arguments";
+const char kNoScreenError[] = "No Screen";
+
+struct _FlFileSelectorPlugin {
+ GObject parent_instance;
+
+ FlPluginRegistrar* registrar;
+
+ // Connection to Flutter engine.
+ FlMethodChannel* channel;
+};
+
+G_DEFINE_TYPE(FlFileSelectorPlugin, fl_file_selector_plugin, G_TYPE_OBJECT)
+
+// Converts a type group received from Flutter into a GTK file filter.
+static GtkFileFilter* type_group_to_filter(FlValue* value) {
+ g_autoptr(GtkFileFilter) filter = gtk_file_filter_new();
+
+ FlValue* label = fl_value_lookup_string(value, kTypeGroupLabelKey);
+ if (label != nullptr && fl_value_get_type(label) == FL_VALUE_TYPE_STRING) {
+ gtk_file_filter_set_name(filter, fl_value_get_string(label));
+ }
+
+ FlValue* extensions = fl_value_lookup_string(value, kTypeGroupExtensionsKey);
+ if (extensions != nullptr &&
+ fl_value_get_type(extensions) == FL_VALUE_TYPE_LIST) {
+ for (size_t i = 0; i < fl_value_get_length(extensions); i++) {
+ FlValue* v = fl_value_get_list_value(extensions, i);
+ const gchar* pattern = fl_value_get_string(v);
+ gtk_file_filter_add_pattern(filter, pattern);
+ }
+ }
+ FlValue* mime_types = fl_value_lookup_string(value, kTypeGroupMimeTypesKey);
+ if (mime_types != nullptr &&
+ fl_value_get_type(mime_types) == FL_VALUE_TYPE_LIST) {
+ for (size_t i = 0; i < fl_value_get_length(mime_types); i++) {
+ FlValue* v = fl_value_get_list_value(mime_types, i);
+ const gchar* pattern = fl_value_get_string(v);
+ gtk_file_filter_add_mime_type(filter, pattern);
+ }
+ }
+
+ return GTK_FILE_FILTER(g_object_ref(filter));
+}
+
+// Creates a GtkFileChooserNative for the given method call details.
+static GtkFileChooserNative* create_dialog(
+ GtkWindow* window, GtkFileChooserAction action, const gchar* title,
+ const gchar* default_confirm_button_text, FlValue* properties) {
+ const gchar* confirm_button_text = default_confirm_button_text;
+ FlValue* value = fl_value_lookup_string(properties, kConfirmButtonTextKey);
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING)
+ confirm_button_text = fl_value_get_string(value);
+
+ g_autoptr(GtkFileChooserNative) dialog =
+ GTK_FILE_CHOOSER_NATIVE(gtk_file_chooser_native_new(
+ title, window, action, confirm_button_text, "_Cancel"));
+
+ value = fl_value_lookup_string(properties, kMultipleKey);
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_BOOL) {
+ gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog),
+ fl_value_get_bool(value));
+ }
+
+ value = fl_value_lookup_string(properties, kInitialDirectoryKey);
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) {
+ gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
+ fl_value_get_string(value));
+ }
+
+ value = fl_value_lookup_string(properties, kSuggestedNameKey);
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) {
+ gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),
+ fl_value_get_string(value));
+ }
+
+ value = fl_value_lookup_string(properties, kAcceptedTypeGroupsKey);
+ if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_LIST) {
+ for (size_t i = 0; i < fl_value_get_length(value); i++) {
+ FlValue* type_group = fl_value_get_list_value(value, i);
+ GtkFileFilter* filter = type_group_to_filter(type_group);
+ if (filter == nullptr) {
+ return nullptr;
+ }
+ gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
+ }
+ }
+
+ return GTK_FILE_CHOOSER_NATIVE(g_object_ref(dialog));
+}
+
+// TODO(stuartmorgan): Move this logic back into method_call_cb once
+// https://github.com/flutter/flutter/issues/88724 is fixed, and test
+// through the public API instead. This only exists to move as much
+// logic as possible behind the private entry point used by unit tests.
+GtkFileChooserNative* create_dialog_for_method(GtkWindow* window,
+ const gchar* method,
+ FlValue* properties) {
+ if (strcmp(method, kOpenFileMethod) == 0) {
+ return create_dialog(window, GTK_FILE_CHOOSER_ACTION_OPEN, "Open File",
+ "_Open", properties);
+ } else if (strcmp(method, kGetDirectoryPathMethod) == 0) {
+ return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+ "Choose Directory", "_Open", properties);
+ } else if (strcmp(method, kGetSavePathMethod) == 0) {
+ return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SAVE, "Save File",
+ "_Save", properties);
+ }
+ return nullptr;
+}
+
+// Shows the requested dialog type.
+static FlMethodResponse* show_dialog(FlFileSelectorPlugin* self,
+ const gchar* method, FlValue* properties,
+ bool return_list) {
+ if (fl_value_get_type(properties) != FL_VALUE_TYPE_MAP) {
+ return FL_METHOD_RESPONSE(fl_method_error_response_new(
+ kBadArgumentsError, "Argument map missing or malformed", nullptr));
+ }
+
+ FlView* view = fl_plugin_registrar_get_view(self->registrar);
+ if (view == nullptr) {
+ return FL_METHOD_RESPONSE(
+ fl_method_error_response_new(kNoScreenError, nullptr, nullptr));
+ }
+ GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(view)));
+
+ g_autoptr(GtkFileChooserNative) dialog =
+ create_dialog_for_method(window, method, properties);
+
+ if (dialog == nullptr) {
+ return FL_METHOD_RESPONSE(fl_method_error_response_new(
+ kBadArgumentsError, "Unable to create dialog from arguments", nullptr));
+ }
+
+ gint response = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog));
+ g_autoptr(FlValue) result = nullptr;
+ if (response == GTK_RESPONSE_ACCEPT) {
+ if (return_list) {
+ result = fl_value_new_list();
+ g_autoptr(GSList) filenames =
+ gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
+ for (GSList* link = filenames; link != nullptr; link = link->next) {
+ g_autofree gchar* filename = static_cast<gchar*>(link->data);
+ fl_value_append_take(result, fl_value_new_string(filename));
+ }
+ } else {
+ g_autofree gchar* filename =
+ gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+ result = fl_value_new_string(filename);
+ }
+ }
+
+ return FL_METHOD_RESPONSE(fl_method_success_response_new(result));
+}
+
+// Called when a method call is received from Flutter.
+static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call,
+ gpointer user_data) {
+ FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(user_data);
+
+ const gchar* method = fl_method_call_get_name(method_call);
+ FlValue* args = fl_method_call_get_args(method_call);
+
+ g_autoptr(FlMethodResponse) response = nullptr;
+ if (strcmp(method, kOpenFileMethod) == 0) {
+ response = show_dialog(self, method, args, true);
+ } else if (strcmp(method, kGetDirectoryPathMethod) == 0 ||
+ strcmp(method, kGetSavePathMethod) == 0) {
+ response = show_dialog(self, method, args, false);
+ } else {
+ response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
+ }
+
+ g_autoptr(GError) error = nullptr;
+ if (!fl_method_call_respond(method_call, response, &error))
+ g_warning("Failed to send method call response: %s", error->message);
+}
+
+static void fl_file_selector_plugin_dispose(GObject* object) {
+ FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(object);
+
+ g_clear_object(&self->registrar);
+ g_clear_object(&self->channel);
+
+ G_OBJECT_CLASS(fl_file_selector_plugin_parent_class)->dispose(object);
+}
+
+static void fl_file_selector_plugin_class_init(
+ FlFileSelectorPluginClass* klass) {
+ G_OBJECT_CLASS(klass)->dispose = fl_file_selector_plugin_dispose;
+}
+
+static void fl_file_selector_plugin_init(FlFileSelectorPlugin* self) {}
+
+FlFileSelectorPlugin* fl_file_selector_plugin_new(
+ FlPluginRegistrar* registrar) {
+ FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(
+ g_object_new(fl_file_selector_plugin_get_type(), nullptr));
+
+ self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
+
+ g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
+ self->channel =
+ fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
+ kChannelName, FL_METHOD_CODEC(codec));
+ fl_method_channel_set_method_call_handler(self->channel, method_call_cb,
+ g_object_ref(self), g_object_unref);
+
+ return self;
+}
+
+void file_selector_plugin_register_with_registrar(
+ FlPluginRegistrar* registrar) {
+ FlFileSelectorPlugin* plugin = fl_file_selector_plugin_new(registrar);
+ g_object_unref(plugin);
+}
diff --git a/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h b/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h
new file mode 100644
index 0000000..e58a78c
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h
@@ -0,0 +1,12 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <flutter_linux/flutter_linux.h>
+
+#include "include/file_selector_linux/file_selector_plugin.h"
+
+// Creates a GtkFileChooserNative for the given method call.
+GtkFileChooserNative* create_dialog_for_method(GtkWindow* window,
+ const gchar* method,
+ FlValue* properties);
diff --git a/packages/file_selector/file_selector_linux/linux/include/file_selector_linux/file_selector_plugin.h b/packages/file_selector/file_selector_linux/linux/include/file_selector_linux/file_selector_plugin.h
new file mode 100644
index 0000000..98e90e5
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/include/file_selector_linux/file_selector_plugin.h
@@ -0,0 +1,31 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef PLUGINS_FILE_SELECTOR_LINUX_FILE_SELECTOR_PLUGIN_H_
+#define PLUGINS_FILE_SELECTOR_LINUX_FILE_SELECTOR_PLUGIN_H_
+
+// A plugin to show native save/open file choosers.
+
+#include <flutter_linux/flutter_linux.h>
+
+G_BEGIN_DECLS
+
+#ifdef FLUTTER_PLUGIN_IMPL
+#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default")))
+#else
+#define FLUTTER_PLUGIN_EXPORT
+#endif
+
+G_DECLARE_FINAL_TYPE(FlFileSelectorPlugin, fl_file_selector_plugin, FL,
+ FILE_SELECTOR_PLUGIN, GObject)
+
+FLUTTER_PLUGIN_EXPORT FlFileSelectorPlugin* fl_file_selector_plugin_new(
+ FlPluginRegistrar* registrar);
+
+FLUTTER_PLUGIN_EXPORT void file_selector_plugin_register_with_registrar(
+ FlPluginRegistrar* registrar);
+
+G_END_DECLS
+
+#endif // PLUGINS_FILE_SELECTOR_LINUX_FILE_SELECTOR_PLUGIN_H_
diff --git a/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc
new file mode 100644
index 0000000..84c55ac
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc
@@ -0,0 +1,171 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "include/file_selector_linux/file_selector_plugin.h"
+
+#include <flutter_linux/flutter_linux.h>
+#include <gtest/gtest.h>
+#include <gtk/gtk.h>
+
+#include "file_selector_plugin_private.h"
+
+// TODO(stuartmorgan): Restructure the helper to take a callback for showing
+// the dialog, so that the tests can mock out that callback with something
+// that changes the selection so that the return value path can be tested
+// as well.
+// TODO(stuartmorgan): Add an injectable wrapper around
+// gtk_file_chooser_native_new to allow for testing values that are given as
+// construction paramaters and can't be queried later.
+
+TEST(FileSelectorPlugin, TestOpenSimple) {
+ g_autoptr(FlValue) args = fl_value_new_map();
+
+ g_autoptr(GtkFileChooserNative) dialog =
+ create_dialog_for_method(nullptr, "openFile", args);
+
+ ASSERT_NE(dialog, nullptr);
+ EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+ GTK_FILE_CHOOSER_ACTION_OPEN);
+ EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+ false);
+}
+
+TEST(FileSelectorPlugin, TestOpenMultiple) {
+ g_autoptr(FlValue) args = fl_value_new_map();
+ fl_value_set_string_take(args, "multiple", fl_value_new_bool(true));
+
+ g_autoptr(GtkFileChooserNative) dialog =
+ create_dialog_for_method(nullptr, "openFile", args);
+
+ ASSERT_NE(dialog, nullptr);
+ EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+ GTK_FILE_CHOOSER_ACTION_OPEN);
+ EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+ true);
+}
+
+TEST(FileSelectorPlugin, TestOpenWithFilter) {
+ g_autoptr(FlValue) type_groups = fl_value_new_list();
+
+ {
+ g_autoptr(FlValue) text_group_mime_types = fl_value_new_list();
+ fl_value_append_take(text_group_mime_types,
+ fl_value_new_string("text/plain"));
+ g_autoptr(FlValue) text_group = fl_value_new_map();
+ fl_value_set_string_take(text_group, "label", fl_value_new_string("Text"));
+ fl_value_set_string(text_group, "mimeTypes", text_group_mime_types);
+ fl_value_append(type_groups, text_group);
+ }
+
+ {
+ g_autoptr(FlValue) image_group_extensions = fl_value_new_list();
+ fl_value_append_take(image_group_extensions, fl_value_new_string("*.png"));
+ fl_value_append_take(image_group_extensions, fl_value_new_string("*.gif"));
+ fl_value_append_take(image_group_extensions,
+ fl_value_new_string("*.jgpeg"));
+ g_autoptr(FlValue) image_group = fl_value_new_map();
+ fl_value_set_string_take(image_group, "label",
+ fl_value_new_string("Images"));
+ fl_value_set_string(image_group, "extensions", image_group_extensions);
+ fl_value_append(type_groups, image_group);
+ }
+
+ {
+ g_autoptr(FlValue) any_group_extensions = fl_value_new_list();
+ fl_value_append_take(any_group_extensions, fl_value_new_string("*"));
+ g_autoptr(FlValue) any_group = fl_value_new_map();
+ fl_value_set_string_take(any_group, "label", fl_value_new_string("Any"));
+ fl_value_set_string(any_group, "extensions", any_group_extensions);
+ fl_value_append(type_groups, any_group);
+ }
+
+ g_autoptr(FlValue) args = fl_value_new_map();
+ fl_value_set_string(args, "acceptedTypeGroups", type_groups);
+
+ g_autoptr(GtkFileChooserNative) dialog =
+ create_dialog_for_method(nullptr, "openFile", args);
+
+ ASSERT_NE(dialog, nullptr);
+ EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+ GTK_FILE_CHOOSER_ACTION_OPEN);
+ EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+ false);
+ // Validate filters.
+ g_autoptr(GSList) type_group_list =
+ gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(dialog));
+ EXPECT_EQ(g_slist_length(type_group_list), 3);
+ GtkFileFilter* text_filter =
+ GTK_FILE_FILTER(g_slist_nth_data(type_group_list, 0));
+ GtkFileFilter* image_filter =
+ GTK_FILE_FILTER(g_slist_nth_data(type_group_list, 1));
+ GtkFileFilter* any_filter =
+ GTK_FILE_FILTER(g_slist_nth_data(type_group_list, 2));
+ // Filters can't be inspected, so query them to see that they match expected
+ // filter behavior.
+ GtkFileFilterInfo text_file_info = {};
+ text_file_info.contains = static_cast<GtkFileFilterFlags>(
+ GTK_FILE_FILTER_DISPLAY_NAME | GTK_FILE_FILTER_MIME_TYPE);
+ text_file_info.display_name = "foo.txt";
+ text_file_info.mime_type = "text/plain";
+ GtkFileFilterInfo image_file_info = {};
+ image_file_info.contains = static_cast<GtkFileFilterFlags>(
+ GTK_FILE_FILTER_DISPLAY_NAME | GTK_FILE_FILTER_MIME_TYPE);
+ image_file_info.display_name = "foo.png";
+ image_file_info.mime_type = "image/png";
+ EXPECT_TRUE(gtk_file_filter_filter(text_filter, &text_file_info));
+ EXPECT_FALSE(gtk_file_filter_filter(text_filter, &image_file_info));
+ EXPECT_FALSE(gtk_file_filter_filter(image_filter, &text_file_info));
+ EXPECT_TRUE(gtk_file_filter_filter(image_filter, &image_file_info));
+ EXPECT_TRUE(gtk_file_filter_filter(any_filter, &image_file_info));
+ EXPECT_TRUE(gtk_file_filter_filter(any_filter, &text_file_info));
+}
+
+TEST(FileSelectorPlugin, TestSaveSimple) {
+ g_autoptr(FlValue) args = fl_value_new_map();
+
+ g_autoptr(GtkFileChooserNative) dialog =
+ create_dialog_for_method(nullptr, "getSavePath", args);
+
+ ASSERT_NE(dialog, nullptr);
+ EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+ GTK_FILE_CHOOSER_ACTION_SAVE);
+ EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+ false);
+}
+
+TEST(FileSelectorPlugin, TestSaveWithArguments) {
+ g_autoptr(FlValue) args = fl_value_new_map();
+ fl_value_set_string_take(args, "initialDirectory",
+ fl_value_new_string("/tmp"));
+ fl_value_set_string_take(args, "suggestedName",
+ fl_value_new_string("foo.txt"));
+
+ g_autoptr(GtkFileChooserNative) dialog =
+ create_dialog_for_method(nullptr, "getSavePath", args);
+
+ ASSERT_NE(dialog, nullptr);
+ EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+ GTK_FILE_CHOOSER_ACTION_SAVE);
+ EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+ false);
+ g_autofree gchar* current_name =
+ gtk_file_chooser_get_current_name(GTK_FILE_CHOOSER(dialog));
+ EXPECT_STREQ(current_name, "foo.txt");
+ // TODO(stuartmorgan): gtk_file_chooser_get_current_folder doesn't seem to
+ // return a value set by gtk_file_chooser_set_current_folder, or at least
+ // doesn't in a test context, so that's not currently validated.
+}
+
+TEST(FileSelectorPlugin, TestGetDirectory) {
+ g_autoptr(FlValue) args = fl_value_new_map();
+
+ g_autoptr(GtkFileChooserNative) dialog =
+ create_dialog_for_method(nullptr, "getDirectoryPath", args);
+
+ ASSERT_NE(dialog, nullptr);
+ EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+ GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
+ EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+ false);
+}
diff --git a/packages/file_selector/file_selector_linux/linux/test/test_main.cc b/packages/file_selector/file_selector_linux/linux/test/test_main.cc
new file mode 100644
index 0000000..7e33b21
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/test/test_main.cc
@@ -0,0 +1,15 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtest/gtest.h>
+#include <gtk/gtk.h>
+
+int main(int argc, char** argv) {
+ gtk_init(0, nullptr);
+
+ testing::InitGoogleTest(&argc, argv);
+ int exit_code = RUN_ALL_TESTS();
+
+ return exit_code;
+}
diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml
new file mode 100644
index 0000000..369fd44
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/pubspec.yaml
@@ -0,0 +1,27 @@
+name: file_selector_linux
+description: Liunx implementation of the file_selector plugin.
+repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_linux
+issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
+version: 0.9.0
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+ flutter: ">=2.8.0"
+
+flutter:
+ plugin:
+ implements: file_selector
+ platforms:
+ linux:
+ pluginClass: FileSelectorPlugin
+ dartPluginClass: FileSelectorLinux
+
+dependencies:
+ cross_file: ^0.3.1
+ file_selector_platform_interface: ^2.1.0
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
diff --git a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart
new file mode 100644
index 0000000..67b9929
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart
@@ -0,0 +1,389 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_selector_linux/file_selector_linux.dart';
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ late FileSelectorLinux plugin;
+ late List<MethodCall> log;
+
+ setUp(() {
+ plugin = FileSelectorLinux();
+ log = <MethodCall>[];
+ plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async {
+ log.add(methodCall);
+ return null;
+ });
+ });
+
+ test('registers instance', () {
+ FileSelectorLinux.registerWith();
+ expect(FileSelectorPlatform.instance, isA<FileSelectorLinux>());
+ });
+
+ group('#openFile', () {
+ test('passes the accepted type groups correctly', () async {
+ final XTypeGroup group = XTypeGroup(
+ label: 'text',
+ extensions: <String>['txt'],
+ mimeTypes: <String>['text/plain'],
+ macUTIs: <String>['public.text'],
+ );
+
+ final XTypeGroup groupTwo = XTypeGroup(
+ label: 'image',
+ extensions: <String>['jpg'],
+ mimeTypes: <String>['image/jpg'],
+ macUTIs: <String>['public.image'],
+ webWildCards: <String>['image/*'],
+ );
+
+ await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('openFile', arguments: <String, dynamic>{
+ 'acceptedTypeGroups': <Map<String, dynamic>>[
+ <String, Object>{
+ 'label': 'text',
+ 'extensions': <String>['*.txt'],
+ 'mimeTypes': <String>['text/plain'],
+ },
+ <String, Object>{
+ 'label': 'image',
+ 'extensions': <String>['*.jpg'],
+ 'mimeTypes': <String>['image/jpg'],
+ },
+ ],
+ 'initialDirectory': null,
+ 'confirmButtonText': null,
+ 'multiple': false,
+ }),
+ ],
+ );
+ });
+
+ test('passes initialDirectory correctly', () async {
+ await plugin.openFile(initialDirectory: '/example/directory');
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('openFile', arguments: <String, dynamic>{
+ 'initialDirectory': '/example/directory',
+ 'confirmButtonText': null,
+ 'multiple': false,
+ }),
+ ],
+ );
+ });
+
+ test('passes confirmButtonText correctly', () async {
+ await plugin.openFile(confirmButtonText: 'Open File');
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('openFile', arguments: <String, dynamic>{
+ 'initialDirectory': null,
+ 'confirmButtonText': 'Open File',
+ 'multiple': false,
+ }),
+ ],
+ );
+ });
+
+ test('throws for a type group that does not support Linux', () async {
+ final XTypeGroup group = XTypeGroup(
+ label: 'images',
+ webWildCards: <String>['images/*'],
+ );
+
+ await expectLater(
+ plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+ throwsArgumentError);
+ });
+
+ test('passes a wildcard group correctly', () async {
+ final XTypeGroup group = XTypeGroup(
+ label: 'any',
+ );
+
+ await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]);
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('openFile', arguments: <String, dynamic>{
+ 'acceptedTypeGroups': <Map<String, dynamic>>[
+ <String, Object>{
+ 'label': 'any',
+ 'extensions': <String>['*'],
+ },
+ ],
+ 'initialDirectory': null,
+ 'confirmButtonText': null,
+ 'multiple': false,
+ }),
+ ],
+ );
+ });
+ });
+
+ group('#openFiles', () {
+ test('passes the accepted type groups correctly', () async {
+ final XTypeGroup group = XTypeGroup(
+ label: 'text',
+ extensions: <String>['txt'],
+ mimeTypes: <String>['text/plain'],
+ macUTIs: <String>['public.text'],
+ );
+
+ final XTypeGroup groupTwo = XTypeGroup(
+ label: 'image',
+ extensions: <String>['jpg'],
+ mimeTypes: <String>['image/jpg'],
+ macUTIs: <String>['public.image'],
+ webWildCards: <String>['image/*'],
+ );
+
+ await plugin.openFiles(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('openFile', arguments: <String, dynamic>{
+ 'acceptedTypeGroups': <Map<String, dynamic>>[
+ <String, Object>{
+ 'label': 'text',
+ 'extensions': <String>['*.txt'],
+ 'mimeTypes': <String>['text/plain'],
+ },
+ <String, Object>{
+ 'label': 'image',
+ 'extensions': <String>['*.jpg'],
+ 'mimeTypes': <String>['image/jpg'],
+ },
+ ],
+ 'initialDirectory': null,
+ 'confirmButtonText': null,
+ 'multiple': true,
+ }),
+ ],
+ );
+ });
+
+ test('passes initialDirectory correctly', () async {
+ await plugin.openFiles(initialDirectory: '/example/directory');
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('openFile', arguments: <String, dynamic>{
+ 'initialDirectory': '/example/directory',
+ 'confirmButtonText': null,
+ 'multiple': true,
+ }),
+ ],
+ );
+ });
+
+ test('passes confirmButtonText correctly', () async {
+ await plugin.openFiles(confirmButtonText: 'Open File');
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('openFile', arguments: <String, dynamic>{
+ 'initialDirectory': null,
+ 'confirmButtonText': 'Open File',
+ 'multiple': true,
+ }),
+ ],
+ );
+ });
+
+ test('throws for a type group that does not support Linux', () async {
+ final XTypeGroup group = XTypeGroup(
+ label: 'images',
+ webWildCards: <String>['images/*'],
+ );
+
+ await expectLater(
+ plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+ throwsArgumentError);
+ });
+
+ test('passes a wildcard group correctly', () async {
+ final XTypeGroup group = XTypeGroup(
+ label: 'any',
+ );
+
+ await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]);
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('openFile', arguments: <String, dynamic>{
+ 'acceptedTypeGroups': <Map<String, dynamic>>[
+ <String, Object>{
+ 'label': 'any',
+ 'extensions': <String>['*'],
+ },
+ ],
+ 'initialDirectory': null,
+ 'confirmButtonText': null,
+ 'multiple': false,
+ }),
+ ],
+ );
+ });
+ });
+
+ group('#getSavePath', () {
+ test('passes the accepted type groups correctly', () async {
+ final XTypeGroup group = XTypeGroup(
+ label: 'text',
+ extensions: <String>['txt'],
+ mimeTypes: <String>['text/plain'],
+ macUTIs: <String>['public.text'],
+ );
+
+ final XTypeGroup groupTwo = XTypeGroup(
+ label: 'image',
+ extensions: <String>['jpg'],
+ mimeTypes: <String>['image/jpg'],
+ macUTIs: <String>['public.image'],
+ webWildCards: <String>['image/*'],
+ );
+
+ await plugin
+ .getSavePath(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('getSavePath', arguments: <String, dynamic>{
+ 'acceptedTypeGroups': <Map<String, dynamic>>[
+ <String, Object>{
+ 'label': 'text',
+ 'extensions': <String>['*.txt'],
+ 'mimeTypes': <String>['text/plain'],
+ },
+ <String, Object>{
+ 'label': 'image',
+ 'extensions': <String>['*.jpg'],
+ 'mimeTypes': <String>['image/jpg'],
+ },
+ ],
+ 'initialDirectory': null,
+ 'suggestedName': null,
+ 'confirmButtonText': null,
+ }),
+ ],
+ );
+ });
+
+ test('passes initialDirectory correctly', () async {
+ await plugin.getSavePath(initialDirectory: '/example/directory');
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('getSavePath', arguments: <String, dynamic>{
+ 'initialDirectory': '/example/directory',
+ 'suggestedName': null,
+ 'confirmButtonText': null,
+ }),
+ ],
+ );
+ });
+
+ test('passes confirmButtonText correctly', () async {
+ await plugin.getSavePath(confirmButtonText: 'Open File');
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('getSavePath', arguments: <String, dynamic>{
+ 'initialDirectory': null,
+ 'suggestedName': null,
+ 'confirmButtonText': 'Open File',
+ }),
+ ],
+ );
+ });
+
+ test('throws for a type group that does not support Linux', () async {
+ final XTypeGroup group = XTypeGroup(
+ label: 'images',
+ webWildCards: <String>['images/*'],
+ );
+
+ await expectLater(
+ plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+ throwsArgumentError);
+ });
+
+ test('passes a wildcard group correctly', () async {
+ final XTypeGroup group = XTypeGroup(
+ label: 'any',
+ );
+
+ await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]);
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('openFile', arguments: <String, dynamic>{
+ 'acceptedTypeGroups': <Map<String, dynamic>>[
+ <String, Object>{
+ 'label': 'any',
+ 'extensions': <String>['*'],
+ },
+ ],
+ 'initialDirectory': null,
+ 'confirmButtonText': null,
+ 'multiple': false,
+ }),
+ ],
+ );
+ });
+ });
+
+ group('#getDirectoryPath', () {
+ test('passes initialDirectory correctly', () async {
+ await plugin.getDirectoryPath(initialDirectory: '/example/directory');
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+ 'initialDirectory': '/example/directory',
+ 'confirmButtonText': null,
+ }),
+ ],
+ );
+ });
+ test('passes confirmButtonText correctly', () async {
+ await plugin.getDirectoryPath(confirmButtonText: 'Open File');
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+ 'initialDirectory': null,
+ 'confirmButtonText': 'Open File',
+ }),
+ ],
+ );
+ });
+ });
+}
diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md
index 0adf1ab..499667d 100644
--- a/packages/file_selector/file_selector_macos/CHANGELOG.md
+++ b/packages/file_selector/file_selector_macos/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.9.0+1
+
+* Updates README for endorsement.
+* Updates `flutter_test` to be a `dev_dependencies` entry.
+
## 0.9.0
* **BREAKING CHANGE**: Methods that take `XTypeGroup`s now throw an
diff --git a/packages/file_selector/file_selector_macos/README.md b/packages/file_selector/file_selector_macos/README.md
index 3241b21..10a5636 100644
--- a/packages/file_selector/file_selector_macos/README.md
+++ b/packages/file_selector/file_selector_macos/README.md
@@ -4,19 +4,12 @@
## Usage
-### Importing the package
-
-This implementation has not yet been endorsed, meaning that you need to
-[depend on `file_selector_macos`][2] in addition to
-[depending on `file_selector`][3].
-
-Once your pubspec includes the macOS implementation, you can use the
-`file_selector` APIs normally. You should not use the `file_selector_macos`
-APIs directly.
+This package is [endorsed][2], which means you can simply use `file_selector`
+normally. This package will be automatically included in your app when you do.
### Entitlements
-You will need to [add an entitlement][4] for either read-only access:
+You will need to [add an entitlement][3] for either read-only access:
```xml
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
@@ -29,6 +22,5 @@
depending on your use case.
[1]: https://pub.dev/packages/file_selector
-[2]: https://pub.dev/packages/file_selector_macos/install
-[3]: https://pub.dev/packages/file_selector/install
-[4]: https://flutter.dev/desktop#entitlements-and-the-app-sandbox
+[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
+[3]: https://flutter.dev/desktop#entitlements-and-the-app-sandbox
diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml
index fc9ca4d..44b842e 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/plugins/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.0
+version: 0.9.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
@@ -21,5 +21,7 @@
file_selector_platform_interface: ^2.1.0
flutter:
sdk: flutter
+
+dev_dependencies:
flutter_test:
sdk: flutter
diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md
index b0e6f0d..6db4fb3 100644
--- a/packages/file_selector/file_selector_windows/CHANGELOG.md
+++ b/packages/file_selector/file_selector_windows/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.9.1+1
+
+* Updates README for endorsement.
+* Updates `flutter_test` to be a `dev_dependencies` entry.
+
## 0.9.1
* Converts the method channel to Pigeon.
diff --git a/packages/file_selector/file_selector_windows/README.md b/packages/file_selector/file_selector_windows/README.md
index 69fb088..c597d70 100644
--- a/packages/file_selector/file_selector_windows/README.md
+++ b/packages/file_selector/file_selector_windows/README.md
@@ -4,16 +4,8 @@
## Usage
-### Importing the package
-
-This implementation has not yet been endorsed, meaning that you need to
-[depend on `file_selector_windows`][2] in addition to
-[depending on `file_selector`][3].
-
-Once your pubspec includes the Windows implementation, you can use the
-`file_selector` APIs normally. You should not use the `file_selector_windows`
-APIs directly.
+This package is [endorsed][2], which means you can simply use `file_selector`
+normally. This package will be automatically included in your app when you do.
[1]: https://pub.dev/packages/file_selector
-[2]: https://pub.dev/packages/file_selector_windows/install
-[3]: https://pub.dev/packages/file_selector/install
+[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml
index 6e39838..e4f38d8 100644
--- a/packages/file_selector/file_selector_windows/pubspec.yaml
+++ b/packages/file_selector/file_selector_windows/pubspec.yaml
@@ -2,7 +2,7 @@
description: Windows implementation of the file_selector plugin.
repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_windows
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
-version: 0.9.1
+version: 0.9.1+1
environment:
sdk: ">=2.12.0 <3.0.0"
@@ -21,10 +21,10 @@
file_selector_platform_interface: ^2.1.0
flutter:
sdk: flutter
- flutter_test:
- sdk: flutter
dev_dependencies:
build_runner: 2.1.11
+ flutter_test:
+ sdk: flutter
mockito: ^5.1.0
pigeon: ^3.2.5
diff --git a/script/configs/exclude_integration_linux.yaml b/script/configs/exclude_integration_linux.yaml
new file mode 100644
index 0000000..a83550e
--- /dev/null
+++ b/script/configs/exclude_integration_linux.yaml
@@ -0,0 +1,3 @@
+# Can't use Flutter integration tests due to native modal UI.
+- file_selector
+- file_selector_linux